/**
 * The ParallelTester class is used to test more than one test concurrently
 */


if (typeof _threadInject !== "undefined") {
  // print( "fork() available!" );

  Thread = function() {
    this.init.apply(this, arguments);
  };
  _threadInject(Thread.prototype);

  ScopedThread = function() {
    this.init.apply(this, arguments);
  };
  ScopedThread.prototype = new Thread(function() {});
  _scopedThreadInject(ScopedThread.prototype);

  fork = function() {
    var t = new Thread(function() {});
    Thread.apply(t, arguments);
    return t;
  };

  // Helper class to generate a list of events which may be executed by a ParallelTester
  EventGenerator = function(me, collectionName, mean, host) {
    this.mean = mean;
    if (host === undefined) {
      host = db.getMongo().host;
    }
    this.events = [me, collectionName, host];
  };

  EventGenerator.prototype._add = function(action) {
    this.events.push([Random.genExp(this.mean), action]);
  };

  EventGenerator.prototype.addInsert = function(obj) {
    this._add("t.insert( " + tojson(obj) + " )");
  };

  EventGenerator.prototype.addRemove = function(obj) {
    this._add("t.remove( " + tojson(obj) + " )");
  };

  EventGenerator.prototype.addUpdate = function(objOld, objNew) {
    this._add("t.update( " + tojson(objOld) + ", " + tojson(objNew) + " )");
  };

  EventGenerator.prototype.addCheckCount = function(count, query, shouldPrint, checkQuery) {
    query = query || {};
    shouldPrint = shouldPrint || false;
    checkQuery = checkQuery || false;
    var action = "assert.eq( " + count + ", t.count( " + tojson(query) + " ) );";
    if (checkQuery) {
      action += " assert.eq( " + count + ", t.find( " + tojson(query) + " ).toArray().length );";
    }
    if (shouldPrint) {
      action += " print( me + ' ' + " + count + " );";
    }
    this._add(action);
  };

  EventGenerator.prototype.getEvents = function() {
    return this.events;
  };

  EventGenerator.dispatch = function() {
    var args = argumentsToArray(arguments);
    var me = args.shift();
    var collectionName = args.shift();
    var host = args.shift();
    var m = new Mongo(host);
    var t = m.getDB("test")[collectionName];
    args.forEach(function(v) {
      sleep(v[0]);
      eval(v[1]); // eslint-disable-line no-eval
    });
  };

  // Helper class for running tests in parallel.  It assembles a set of tests
  // and then calls assert.parallelTests to run them.
  ParallelTester = function() {
    assert.neq(db.getMongo().writeMode(), "legacy", "wrong shell write mode");
    this.params = [];
  };

  ParallelTester.prototype.add = function(fun, args) {
    args = args || [];
    args.unshift(fun);
    this.params.push(args);
  };

  ParallelTester.prototype.run = function(msg, newScopes) {
    newScopes = newScopes || false;
    assert.parallelTests(this.params, msg, newScopes);
  };

  // creates lists of tests from jstests dir in a format suitable for use by
  // ParallelTester.fileTester.  The lists will be in random order.
  // n: number of lists to split these tests into
  ParallelTester.createJstestsLists = function(n) {
    var params = [];
    for (var i = 0; i < n; ++i) {
      params.push([]);
    }

    var makeKeys = function(a) {
      var ret = {};
      a.forEach(function(v) {
        ret[v] = 1;
      });
      return ret;
    };

    // some tests can't run in parallel with most others
    var skipTests = makeKeys([
      "dbadmin.js",
      "cursor8.js",
      "recstore.js",
      "extent.js",
      "indexb.js",

      // tests turn on profiling
      "profile1.js",
      "profile3.js",
      "profile4.js",
      "profile5.js",

      "mr_drop.js",
      "mr3.js",
      "indexh.js",
      "apitest_db.js",
      "evalb.js",
      "evald.js",
      "evalf.js",
      "killop.js",
      "run_program1.js",
      "notablescan.js",
      "drop2.js",
      "dropdb_race.js",
      "fsync2.js", // May be placed in serialTestsArr once SERVER-4243 is fixed.
      "bench_test1.js",
      "padding.js",
      "queryoptimizera.js",
      "loglong.js", // log might overflow before
      // this has a chance to see the message
      "connections_opened.js", // counts connections, globally
      "opcounters_write_cmd.js",
      "currentop.js", // SERVER-8673, plus rwlock yielding issues
      "set_param1.js", // changes global state
      "geo_update_btree2.js", // SERVER-11132 test disables table scans
      "update_setOnInsert.js", // SERVER-9982
    ]);

    var parallelFilesDir = "jstests/core";

    // some tests can't be run in parallel with each other
    var serialTestsArr = [
      parallelFilesDir + "/fsync.js",
      parallelFilesDir + "/auth1.js",

      // These tests expect the profiler to be on or off at specific points
      // during the test run.
      parallelFilesDir + "/cursor6.js",
      parallelFilesDir + "/profile2.js",
      parallelFilesDir + "/updatee.js"
    ];
    var serialTests = makeKeys(serialTestsArr);

    // prefix the first thread with the serialTests
    // (which we will exclude from the rest of the threads below)
    params[0] = serialTestsArr;
    var files = listFiles(parallelFilesDir);
    files = Array.shuffle(files);

    i = 0;
    files.forEach(function(x) {
      if ((/[/\\]_/.test(x.name)) ||
              (!/\.js$/.test(x.name)) ||
              (x.name.match(parallelFilesDir + "/(.*\\.js)")[1] in skipTests) ||
              (x.name in serialTests)) {
        print(" >>>>>>>>>>>>>>> skipping " + x.name);
        return;
      }
      // add the test to run in one of the threads.
      params[i % n].push(x.name);
      ++i;
    });

    // randomize ordering of the serialTests
    params[0] = Array.shuffle(params[0]);

    params.forEach(function(param) {
      param.unshift(i);
    });

    return params;
  };

  // runs a set of test files
  // first argument is an identifier for this tester, remaining arguments are file names
  ParallelTester.fileTester = function() {
    var args = argumentsToArray(arguments);
    var suite = args.shift();
    args.forEach(function(x) {
      print("         S" + suite + " Test : " + x + " ...");
      var time = Date.timeFunc(function() {
        load(x);
      }, 1);
      print("         S" + suite + " Test : " + x + " " + time + "ms");
    });
  };

  // params: array of arrays, each element of which consists of a function followed
  // by zero or more arguments to that function.  Each function and its arguments will
  // be called in a separate thread.
  // msg: failure message
  // newScopes: if true, each thread starts in a fresh scope
  assert.parallelTests = function(params, msg, newScopes) {
    newScopes = newScopes || false;
    var wrapper = function(fun, argv) {
      // TODO: this doesn't need to use eval
      eval( // eslint-disable-line no-eval
        "var z = function() {" +
            "var __parallelTests__fun = " + fun.toString() + ";" +
            "var __parallelTests__argv = " + tojson(argv) + ";" +
            "var __parallelTests__passed = false;" +
            "try {" +
               "__parallelTests__fun.apply( 0, __parallelTests__argv );" +
               "__parallelTests__passed = true;" +
            "} catch ( e ) {" +
               "print('');" +
               "print( '********** Parallel Test FAILED: ' + tojson(e) );" +
               "print('');" +
            "}" +
            "return __parallelTests__passed;" +
          "}"
      );
      return z;
    };
    var runners = [];
    params.forEach(function(param) {
      var test = param.shift();
      var t;
      if (newScopes) {
        t = new ScopedThread(wrapper(test, param));
      } else {
        t = new Thread(wrapper(test, param));
      }
      runners.push(t);
    });

    runners.forEach(function(x) {
      x.start();
    });
    var nFailed = 0;
    // v8 doesn't like it if we exit before all threads are joined (SERVER-529)
    runners.forEach(function(x) {
      if (!x.returnData()) {
        ++nFailed;
      }
    });
    assert.eq(0, nFailed, msg);
  };
}
